// Linux Device Driver Template/Skeleton with mmap
// Kernel Module

#include <linux/module.h>
#include <linux/init.h>
#include <linux/version.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/vmalloc.h>
#include <linux/mman.h>
#include <linux/slab.h>

#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
#include <linux/wrapper.h>
#endif

//mknod /dev/mmapdrv c 240 0

#define SKELETON_MAJOR 240
#define SKELETON_NAME "mmapdrv"
#define CASE1 1
#define CASE2 2

static unsigned int counter = 0;
static char string [128];
static int data;

#define USEASCII

#ifdef USEASCII
static char *kmalloc_area = NULL;
static char *kmalloc_ptr = NULL;
#else
static unsigned int *kmalloc_area = NULL;
static unsigned int *kmalloc_ptr = NULL;
#endif

#define LEN (64*1024)
unsigned long virt_addr;

DECLARE_WAIT_QUEUE_HEAD(mmapdrv_wait);
static int data_not_ready = 0;

// open function - called when the "file" /dev/mmapdrv is opened in userspace
static int mmapdrv_open (struct inode *inode, struct file *file) 
{
	printk("mmapdrv_openn");
	// we could do some checking on the flags supplied by "open"
	// i.e. O_NONBLOCK
	// -> set some flag to disable interruptible_sleep_on in mmapdrv_read
	return 0;
}

// close function - called when the "file" /dev/mmapdrv is closed in userspace  
static int mmapdrv_release (struct inode *inode, struct file *file) 
{
	printk("mmapdrv_releasen");
	return 0;
}

// read function called when from /dev/mmapdrv is read
static ssize_t mmapdrv_read (struct file *file, char *buf,
		size_t count, loff_t *ppos) 
{
	int len, err;
	
	// check if we have data - if not, sleep
	// wake up in interrupt_handler
	while (data_not_ready) {
		interruptible_sleep_on(&mmapdrv_wait);
	}
	//data_not_ready = 1;
	
	if( counter <= 0 ) 
		return 0;
	err = copy_to_user(buf,string,counter);
	if (err != 0)
		return -EFAULT;
	len  = counter;
	counter = 0;
	return len;
}

// write function called when to /dev/mmapdrv is written
static ssize_t mmapdrv_write (struct file *file, const char *buf,
		size_t count, loff_t *ppos) 
{
	int err;
	err = copy_from_user(string,buf,count);
	if (err != 0)
		return -EFAULT;
	counter += count;
	return count;
}

// ioctl - I/O control
static int mmapdrv_ioctl(struct inode *inode, struct file *file,
		unsigned int cmd, unsigned long arg) {
	int retval = 0;
	switch ( cmd ) {
		case CASE1:/* for writing data to arg */
			if (copy_from_user(&data, (int *)arg, sizeof(int)))
			return -EFAULT;
			break;
		case CASE2:/* for reading data from arg */
			if (copy_to_user((int *)arg, &data, sizeof(int)))
			return -EFAULT;
			break;
		default:
			retval = -EINVAL;
	}
	return retval;
}

#ifndef VMALLOC_VMADDR
#define VMALLOC_VMADDR(x) ((unsigned long)(x))
#endif

// From: http://www.scs.ch/~frey/linux/memorymap.html
volatile void *virt_to_kseg(volatile void *address) {
	pgd_t *pgd; pmd_t *pmd; pte_t *ptep, pte;
	unsigned long va, ret = 0UL;
	va=VMALLOC_VMADDR((unsigned long)address);
	/* get the page directory. Use the kernel memory map. */
	pgd = pgd_offset_k(va);
	/* check whether we found an entry */
	if (!pgd_none(*pgd)) {
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
		/* get the page middle directory */
		pmd = pmd_offset(pgd, va);
#else
		// I'm not sure if we need this, or the line for 2.4
		//    above will work reliably too
		// If you know, please email me :-)
		pud_t *pud = pud_offset(pgd, va);		
		pmd = pmd_offset(pud, va);
#endif
		/* check whether we found an entry */
		if (!pmd_none(*pmd)) {
			/* get a pointer to the page table entry */
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
			ptep = pte_offset(pmd, va);
#else
			ptep = pte_offset_map(pmd, va);
#endif
			pte = *ptep;
			/* check for a valid page */
			if (pte_present(pte)) {
				/* get the address the page is refering to */
				ret = (unsigned long)page_address(pte_page(pte));
				/* add the offset within the page to the page address */
				ret |= (va & (PAGE_SIZE -1));
			}
		}
	}
	return((volatile void *)ret);
}

static int mmapdrv_mmap(struct file * filp, struct vm_area_struct * vma) 
{
	int ret;
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
	ret = remap_page_range(vma->vm_start,
			virt_to_phys((void*)((unsigned long)kmalloc_area)),
			vma->vm_end-vma->vm_start,
			PAGE_SHARED);
//			       vma->vm_page_prot);
#else
        ret = remap_pfn_range(vma,
               vma->vm_start,
               virt_to_phys((void*)((unsigned long)kmalloc_area)) >> PAGE_SHIFT,
               vma->vm_end-vma->vm_start,
               PAGE_SHARED);
//               vma->vm_page_prot); 
#endif
	if(ret != 0) {
		return -EAGAIN;
	}
	return 0;
}

// define which file operations are supported
struct file_operations mmapdrv_fops = {
	.owner	=	THIS_MODULE,
	.llseek	=	NULL,
	.read	=	mmapdrv_read,
	.write	=	mmapdrv_write,
	.readdir	=	NULL,
	.poll		=	NULL,
	.ioctl		=	mmapdrv_ioctl,
	.mmap	=	mmapdrv_mmap,
	.open	=	mmapdrv_open,
	.flush	=	NULL,
	.release	=	mmapdrv_release,
	.fsync	=	NULL,
	.fasync	=	NULL,
	.lock		=	NULL,
	//.readv	=	NULL,
	//.writev	=	NULL,
};

// initialize module
static int __init mmapdrv_init_module (void) {
	int i;
#ifndef USEASCII
	int tmp, tmp2;
#endif
	printk("initializing modulen\n");
	
	i = register_chrdev (SKELETON_MAJOR, SKELETON_NAME, &mmapdrv_fops);
	if (i != 0) 
		return - EIO;
	
	// reserve memory with kmalloc - Allocating Memory in the Kernel
	kmalloc_ptr = kmalloc(LEN + 2 * PAGE_SIZE, GFP_KERNEL);
	if (!kmalloc_ptr) {
		printk("kmalloc failedn\n");
		return 0;
	}
#ifdef USEASCII
	kmalloc_area = (char *)			(((unsigned long)kmalloc_ptr + PAGE_SIZE -1) & PAGE_MASK);
#else
	kmalloc_area = (unsigned int *)    (((unsigned long)kmalloc_ptr + PAGE_SIZE -1) & PAGE_MASK);
#endif
	for (virt_addr=(unsigned long)kmalloc_area; virt_addr < (unsigned long)kmalloc_area + LEN;
		virt_addr+=PAGE_SIZE) {
			// reserve all pages to make them remapable
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
			mem_map_reserve(virt_to_page(virt_addr));
#else
			SetPageReserved(virt_to_page(virt_addr));
#endif
	}
	printk("kmalloc_area at 0x%p (phys 0x%lx)\n", kmalloc_area,
		virt_to_phys((void *)virt_to_kseg(kmalloc_area)));

#ifdef USEASCII
	// fill allocated memory with 0123456789 ascii
	for( i = 48; i < 58; i++) {
		kmalloc_ptr[i-48] = (char)i;
	}
	i = 0;
	kmalloc_ptr[58-48] = (char)i;
#else
	// fill allocated memory with integers
	tmp = sizeof(int);
	for( i = 0; i < (10 * tmp); i = i + tmp) {
		kmalloc_ptr[i] = (unsigned int)i;
      
		tmp2 = (unsigned int)kmalloc_ptr[i];
		printk("kmalloc_ptr[%d]=%d\n", i, tmp2);
	}
#endif

	return 0;
}

// close and cleanup module
static void __exit mmapdrv_cleanup_module (void) {
	printk("cleaning up modulen");
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
	for (virt_addr=(unsigned long)kmalloc_area; virt_addr < (unsigned long)kmalloc_area + LEN;
		virt_addr+=PAGE_SIZE) {
			// clear all pages
			ClearPageReserved(virt_to_page(virt_addr));
	}
#endif
	kfree(kmalloc_ptr);
	unregister_chrdev (SKELETON_MAJOR, SKELETON_NAME);
}

module_init(mmapdrv_init_module);
module_exit(mmapdrv_cleanup_module);
MODULE_AUTHOR("www.captain.at");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Linux Device Driver Template with MMAP");
